# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import gc
import os.path
import sys
import random
import platform
import time

import pandas as pd
import warnings

warnings.filterwarnings('ignore')
pd.set_option('display.max_rows', 1000)
pd.set_option('expand_frame_repr', False)  # 当列太多时不换行
pd.set_option('display.unicode.ambiguous_as_wide', True)  # 设置命令行输出时的列对齐功能
pd.set_option('display.unicode.east_asian_width', True)
from api.market import *
from api.position import *
from api.trade import *
from utils.functions import *
from utils.commons import *
from utils.dingding import *
from utils.datatools import *
from config import *
from concurrent.futures import ThreadPoolExecutor
from utils.statistics import record_after_order_pos, record_before_order_pos

# 解决pm2在win系统下的乱码问题
os_type = platform.system()
if os_type == 'Windows':  # win执行
    sys.stdout.reconfigure(encoding='utf-8')
    # 若上面sys.stdout = ... 这一行不行，可以试试下一行
    # import io
    # sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8')

# 构建线程池
executor = ThreadPoolExecutor()
# 获取当前环境下的python解释器
python_exec = sys.executable
# 获取数据的公共交易所对象
common_exchange = ccxt.binance(exchange_basic_config)


def proceed_swap_order(exchange, symbol_order, account_info):
    """
    处理合约下单
    :param exchange:        交易所对象
    :param symbol_order:    下单数据
    :param account_info:    用户配置
    """
    # =====获取合约与现货的信息。最小下单量，下单精度等
    # =获取U本位合约交易对的信息
    swap_symbol_list, swap_min_qty, swap_price_precision, swap_min_notional = load_market(common_exchange,
                                                                                          symbol_type='swap')

    max_one_order_amount = account_info['max_one_order_amount']  # 最大拆单金额
    twap_interval = account_info['twap_interval']  # 下单间隔时间
    wechat_webhook_url = account_info['wechat_webhook_url']  # 当前账号的机器人

    # ===使用twap算法拆分订单
    swap_order = symbol_order[symbol_order['symbol_type'] == 'swap']
    twap_swap_symbol_info_list = get_twap_symbol_info_list(swap_order, max_one_order_amount)

    # ===遍历下单
    for i in range(len(twap_swap_symbol_info_list)):
        # 获取合约币种的最新价格
        symbol_last_price = fetch_binance_ticker_data(exchange)
        # 逐批下单
        place_order(exchange, twap_swap_symbol_info_list[i], symbol_last_price, swap_min_qty,
                    swap_price_precision, swap_min_notional, symbol_type='swap', wechat_webhook_url=wechat_webhook_url)
        # 下单间隔
        print(f'等待 {twap_interval}s 后继续下单')
        time.sleep(twap_interval)
    # 清理数据
    del swap_symbol_list, swap_min_qty, swap_price_precision, swap_min_notional


def proceed_spot_order(exchange, symbol_order, account_info, select_coin, is_only_sell=False):
    """
    处理现货下单
    :param exchange:        交易所对象
    :param symbol_order:    下单数据
    :param account_info:    用户配置
    :param select_coin:     选币数据
    :param is_only_sell:    是否仅仅进行卖单交易
    """
    # =====获取合约与现货的信息。最小下单量，下单精度等
    # =加载现货交易对信息
    spot_symbol_list, spot_min_qty, spot_price_precision, spot_min_notional = load_market(common_exchange,
                                                                                          symbol_type='spot')

    max_one_order_amount = account_info['max_one_order_amount']  # 最大拆单金额
    twap_interval = account_info['twap_interval']  # 下单间隔时间
    wechat_webhook_url = account_info['wechat_webhook_url']  # 当前账号的机器人

    # ===现货处理
    spot_order = symbol_order[symbol_order['symbol_type'] == 'spot']
    # 判断是否需要现货下单
    if not spot_order.empty:  # 如果使用了现货数据实盘，则进行现货下单
        # =使用twap算法拆分订单
        short_order = spot_order[spot_order['实际下单资金'] <= 0]
        long_order = spot_order[spot_order['实际下单资金'] > 0]
        # 判断是否只卖现货
        if is_only_sell:  # 如果是仅仅交易卖单，只进行拆单
            twap_spot_short_list = get_twap_symbol_info_list(short_order, max_one_order_amount)
            twap_spot_symbol_info_list = twap_spot_short_list
        else:  # 如果是仅仅交易买单，拆单，划转资金去现货账户
            twap_spot_long_list = get_twap_symbol_info_list(long_order, max_one_order_amount)
            twap_spot_symbol_info_list = twap_spot_long_list

            # =划转资金到现货账户
            time.sleep(5)
            transfer_amount = calc_spot_need_usdt_amount(account_info, select_coin, spot_order)
            print('transfer_amount', transfer_amount)
            if transfer_amount > 0:
                # 转资金到现货账户
                bn_transfer_u_to_spot(exchange, round(transfer_amount - 1, 1))

        # =现货遍历下单
        for i in range(len(twap_spot_symbol_info_list)):
            # 获取现货币种的最新价格
            symbol_last_price = fetch_binance_ticker_data(exchange, symbol_type='spot')
            # 逐批下单
            place_order(exchange, twap_spot_symbol_info_list[i], symbol_last_price, spot_min_qty,
                        spot_price_precision, spot_min_notional, symbol_type='spot',
                        wechat_webhook_url=wechat_webhook_url)
            # 下单间隔
            print(f'等待 {twap_interval}s 后继续下单')
            time.sleep(twap_interval)

        # =将现货中的U转入的合约账号
        spot_position = get_spot_position(exchange)
        if 'USDT' in spot_position['symbol'].to_list():
            usdt = spot_position.loc[spot_position['symbol'] == 'USDT', '当前持仓量'].iloc[0]
            # 转资金到合约账户
            bn_transfer_spot_to_u(exchange, round(usdt - 1, 1))
    # 清理数据
    del spot_symbol_list, spot_min_qty, spot_price_precision, spot_min_notional


def run():
    while True:
        # =====sleep直到该小时开始。但是会随机提前几分钟。
        if not is_debug:  # 非调试模式，需要正常进行sleep
            random_time = random.randint(1 * 60, 2 * 60)  # 不建议使用负数。小时级别提前时间可以设置短一点
            run_time = sleep_until_run_time('5m', if_sleep=True, cheat_seconds=random_time)  # 每小时运行
        else:  # 调试模式，不进行sleep，直接继续往后运行
            random_time = None
            # run_time = sleep_until_run_time('1h', if_sleep=False, cheat_seconds=0)  # 每小时运行
            # 以下代码可以测试的时候使用(UTC0点 日K才走完，国内时间对应是 早上8点)
            run_time = datetime.strptime('2024-01-28 09:30:00', "%Y-%m-%d %H:%M:%S")

        # =====获取账户信息
        update_account_info = get_account_info(account_config.copy())
        # 判断是否没能成功读取任一账户
        if not len(update_account_info.keys()):  # 如果update_account_info数据为空，表示更新账户信息失败
            print('所有账户更新信息失败，60s后重试······')
            time.sleep(60)
            continue

        # =====判断指数的本地文件有没有更新成功。
        flag = check_flag(run_time)
        # 判断数据是否更新好了
        if not flag:  # 没有更新好，跳过当前下单
            continue

        # =====遍历所有账号。计算选币，下单
        for account_name, account_info in update_account_info.items():
            # 获取账号的配置
            account_info['account_name'] = account_name  # 账户信息里面，追加一下用户名称
            exchange = account_info['exchange']  # 交易所对象
            is_day_period = account_info['is_day_period']  # 是否是日线级别策略
            has_spot_strategy = account_info['has_spot_strategy']  # 是否含有现货策略
            spot_position = account_info['spot_position']  # 现货持仓
            swap_position = account_info['swap_position']  # 合约持仓
            if_rebalance = account_info['if_rebalance']  # 是否开启rebalance
            hour_offset = account_info['hour_offset']  # 分钟偏移

            # 不是当前hour_offset提前的30分钟 , 不是轮动策略， 未开启提前计算功能
            if run_time.minute == (int(hour_offset[:-1]) - 30 % 60) and (not account_info['is_shift'] or not account_info['is_ahead_calc']):
                print(f'账号：{account_name}，当前不提前30分钟执行操作，跳过···')
                continue

            # 判断当前是否提前计算资金曲线
            if run_time.minute != (int(hour_offset[:-1]) - 30 % 60) and run_time.minute != int(hour_offset[:-1]):
                print(f'账号：{account_name}，当前不是指定提前计算资金曲线的时间，跳过···')
                continue

            # 判断当前策略持仓是否是日线
            # 如果是日线持仓则需要判断下单时间，小时级别持仓不需要判断下单时间
            if is_day_period and run_time.hour != utc_offset % 24:  # 只有当前小时是utc0点的时候，才会下单
                print(f'账号：{account_name}，当前不是需要下单的时间，跳过···')
                continue

            # 检查轮动策略是否是首次启动
            is_first_run = check_first_run_shift(account_info)

            # ===撤销所有币种挂单
            # 合约撤单
            cancel_swap_all_orders(exchange)
            # 现货撤单
            cancel_spot_all_orders(exchange)

            # ===读取实盘所需数据
            # 读取数据中心数据
            s_time = datetime.now()
            symbol_swap_candle_data = load_data('swap', run_time, account_info, njob=njob)
            if has_spot_strategy:
                symbol_spot_candle_data = load_data('spot', run_time, account_info, njob=njob)
            else:
                symbol_spot_candle_data = dict()
            print('完成读取数据中心数据，花费时间：', datetime.now() - s_time)

            # ===选币数据整理 & 选币
            s_time = datetime.now()
            select_coin = cal_factor_and_select_coin(symbol_spot_candle_data, symbol_swap_candle_data, run_time,
                                                     account_info, special_symbol_dict)
            print('完成选币数据整理 & 选币，花费时间：', datetime.now() - s_time)
            print('选币结果：\n', select_coin)
            # 如果选币数据为空，并且当前账号没有任何持仓，直接跳过后续操作
            if select_coin.empty and spot_position.empty and swap_position.empty:
                continue
            if run_time.minute != int(hour_offset[:-1]):
                print(f'账号：{account_name}，当前不是下单的时间，跳过···')
                continue
            # 首次启动轮动，则跳过第一次下单
            if is_first_run:
                continue

            # ===保存并合并本地选币文件
            select_coin = save_and_merge_select(select_coin, account_name, account_info, run_time, if_rebalance)
            print('计算下单金额及是否re结果：\n', select_coin)

            # ===开始计算具体下单信息
            symbol_order = cal_order_amount(select_coin, account_info)
            print('下单信息：\n', symbol_order)
            # 调试模式，打印下单信息之后即可退出程序
            if is_debug:
                continue

            # 下单前记录持仓
            record_before_order_pos(exchange, run_time, account_name, has_spot_strategy)

            # ===下单
            proceed_spot_order(exchange, symbol_order, account_info, select_coin, is_only_sell=True)
            proceed_swap_order(exchange, symbol_order, account_info)
            proceed_spot_order(exchange, symbol_order, account_info, select_coin, is_only_sell=False)

            # ===保存选币数据
            save_select_coin(select_coin, run_time, account_name)

            # ===保存下单数据
            save_symbol_order(symbol_order, run_time, account_name)

            # 记录下单后持仓
            record_after_order_pos(exchange, run_time, account_name, has_spot_strategy)

        # 本次循环结束
        print('-' * 20, '本次循环结束，%f秒后进入下一次循环' % 20, '-' * 20)
        print('\n')
        time.sleep(20)

        # =====自动同步时间
        # 当前服务器时间与交易所时间偏差超过60s
        if glob_var.diff_timestamp / 1000 > 60:
            refresh_diff_time(common_exchange)
            if os_type == 'Windows':  # win执行
                msg = '当前服务器时间与交易所时间偏差超过60s, win用户请手动运行 sync_tim.py'
                print(msg)
                send_wechat_work_msg(msg, error_webhook_url)
            elif os_type == 'Linux' or os_type == 'Darwin':  # linux和mac执行
                from sync_time import sync_time_ntp
                sync_time_ntp()  # 指定更新时间
                refresh_diff_time(common_exchange)

        # =====运行监测脚本
        if run_time.minute in [int(_['hour_offset'][:-1]) for _ in update_account_info.values()]:
            time.sleep(random_time if random_time else 120)  # 提前随机多久，就休息多久，默认2分钟
            executor.submit(run_statistics, run_time)  # 异步执行

        # =====清理数据
        del update_account_info, flag
        gc.collect()  # 强制执行回收一下

        # 补偿当前时间与run_time之间的差值
        remedy_until_run_time(run_time)


def run_statistics(run_time):
    print('==============运行开始==================')
    os.system(f'{python_exec} {os.path.join(root_path, "utils", "statistics.py")} {int(run_time.timestamp())}')
    print('==============运行结束==================')


if __name__ == '__main__':
    # =====刷新与交易所的时差
    refresh_diff_time(common_exchange)
    # =====遍历账号进行调整
    for name, account in account_config.items():
        # ===检查杠杆
        check_leverage(account)
        # ===检查并且设置持仓模式：单向持仓
        check_position_side(account['exchange'])
        # ===检查联合保证金模式
        check_multi_assets_margin(account['exchange'])
        # ===多账户之间，停顿一下
        time.sleep(5)
    while True:
        try:
            run()
        except Exception as err:
            msg = '系统出错，10s之后重新运行，出错原因: ' + str(err)
            print(msg)
            print(traceback.format_exc())
            send_wechat_work_msg(msg, error_webhook_url)
            time.sleep(10)
